package com.newmainsoftech.dao.entity.relation;

import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Iterator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HoldeeIteratorProxyImpl<E extends HoldeeSide<R>, R extends HolderSide<E>> 
implements HoldeeIteratorProxy {
	private Logger logger = LoggerFactory.getLogger( getClass());
		protected Logger getLogger() {
			return logger;
		}
		
	/*
	 * An iterator from <code>Collection</code> instance what holds holdee entity instances 
	 * (child side of bi-directional relation); usually stored in a member field of holder entity 
	 * class (though it is not necessary to be.)
	 */
	private Iterator<E> delegateIterator;
		protected Iterator<E> getDelegateIterator() {
			return delegateIterator;
		}
		protected void setDelegateIterator( final Iterator<E> delegateIterator) {
			this.delegateIterator = delegateIterator;
		}
		
	private R holderEntity;
		protected R getHolderEntity() {
			return holderEntity;
		}
		protected void setHolderEntity( final R holderEntity) {
			if ( holderEntity == null) {
				throw new IllegalArgumentException(
						MessageFormat.format(
								"Value of {0} argument must be an instance of {1} but null.",
								"holderEntity",
								HolderSide.class.getSimpleName())
						);
			}
			if ( holderEntity.getHoldees() == null) {
				throw new NullPointerException(
						MessageFormat.format(
								"{0} method on {1} did not return an instance of collection but null.",
								getGetHoldeesMethod(),
								holderEntity)
						);
			}
			
			this.holderEntity = holderEntity;
		}
		
	private R defaultHolderEntity = null;
		public R getDefaultHolderEntity() {
			return defaultHolderEntity;
		}
		protected void setDefaultHolderEntity( final R defaultHolderEntity) {
			this.defaultHolderEntity = defaultHolderEntity;
		}
		
	private boolean isOrphanRemovalAttributeOn = false;	
		public boolean isOrphanRemovalAttributeOn() {
			return isOrphanRemovalAttributeOn;
		}
		protected void setOrphanRemovalAttributeOn( boolean isOrphanRemovalAttributeOn) {
			this.isOrphanRemovalAttributeOn = isOrphanRemovalAttributeOn;
		}

	private boolean isOkToNullifyHolderAtRemoval 
	= EntityBiLinkCollectionProxyImpl.IsOkToNullifyHolderAtRemovalDefaultValue;
		public boolean isOkToNullifyHolderAtRemoval() {
			return isOkToNullifyHolderAtRemoval;
		}
		public void setOkToNullifyHolderAtRemoval( boolean isOkToNullifyHolderAtRemoval) {
			this.isOkToNullifyHolderAtRemoval = isOkToNullifyHolderAtRemoval;
		}

	private Method getHoldeesMethod;
		{
			try {
				getHoldeesMethod 
				= HolderSide.class.getDeclaredMethod( "getHoldees", new Class<?>[]{});
			}
			catch( Exception exception) {
				if ( RuntimeException.class.isInstance( exception)) {
					throw (RuntimeException)exception;
				}
				else {
					throw new RuntimeException( exception);
				}
			}
		}
		protected Method getGetHoldeesMethod() {
			return getHoldeesMethod;
		}
		
	public HoldeeIteratorProxyImpl( 
			final R holderEntity, 
			final R defaultHolderEntity, 
			boolean isOrphanRemovalAttributeOn, 
			boolean isOkToNullifyHolderAtRemoval) {
		setHolderEntity( holderEntity);
				// holderEntity is assured as not null.
		if ( holderEntity.equals( defaultHolderEntity)) {
			throw new IllegalArgumentException(
					MessageFormat.format(
							"Values of {0} argument and {1} argument should be different but {2} and " 
							+ "{3} are equals.",
							"holderEntity",
							"defaultHolderEntity", 
							holderEntity, 
							defaultHolderEntity)
					);
		}
		setDefaultHolderEntity( defaultHolderEntity);
		
		setOrphanRemovalAttributeOn( isOrphanRemovalAttributeOn);
		
		setOkToNullifyHolderAtRemoval( isOkToNullifyHolderAtRemoval);
		
		final Collection<E> holdees = holderEntity.getHoldees();
			if ( holdees == null) {
				throw new NullPointerException(
						MessageFormat.format(
								"Value being returned by {0}.{1} method on {2} " 
								+ "must be a {3} instance but null.",
								getGetHoldeesMethod().getDeclaringClass().getSimpleName(),
								getGetHoldeesMethod().getName(),
								holderEntity, 
								Collection.class.getSimpleName())
						);
			}
		setDelegateIterator( holdees.iterator());
	}

	private E holdeeEntity;
		protected E getHoldeeEntity() {
			return holdeeEntity;
		}
		protected void setHoldeeEntity( final E holdeeEntity) {
			this.holdeeEntity = holdeeEntity;
		}
		
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object interNext( final Object proxy, final Method method) {
		E holdee = getDelegateIterator().next();
			setHoldeeEntity( holdee);
		return holdee;
	}
	/**
	 * {@inheritDoc}
	 * This will also establish relation between <code>{@link HoldeeSide}</code> entity (what is currently 
	 * pointed by iterator and being removed from underneath collection) and <code>{@link HolderSide}</code> 
	 * entity being returned by <code>{@link #getDefaultHolderEntity()}</code> method at removal of 
	 * <code>{@link HoldeeSide}</code> entity from the underneath collection, if 
	 * <code>{@link #getDefaultHolderEntity()}</code> method returns an instance of 
	 * <code>{@link HolderSide}</code> entity but null. 
	 * Regarding that matter, please be advised the followings below, otherwise, there is possibility of 
	 * ending up to duplicate <code>{@link HoldeeSide}</code> entities in the collection accessible via 
	 * <code>{@link HolderSide#getHoldees()}</code> method on <code>{@link HolderSide}</code> entity what 
	 * is returned by <code>{@link #getHolderEntity()}</code> method, unless the type of that collection 
	 * is hash base:
	 * <ul>
	 *  <li>About the implementation of <code>{@link HoldeeSide#setHolderEntity(Object)}</code> method, 
	 *   <code>add</code> method of the collection being returned by <code>{@link HolderSide#getHoldees()}
	 *   </code> method on the <code>{@link HolderSide}</code> entity provided as the argument of 
	 *   <code>{@link HoldeeSide#setHolderEntity(Object)}</code> method should not be called.<br />
	 *   Otherwise, 
	 *  </li>
	 *  <li>Collection being returned by <code>{@link HolderSide#getHoldees()}</code> method on 
	 *  <code>{@link HolderSide}</code> entity what is returned by <code>{@link #getHolderEntity()}</code> 
	 *  method should not contain the <code>{@link HoldeeSide}</code> entity pointed currently by iterator 
	 *  and being removed from the underneath collection. 
	 *  </li>
	 * </ul>
	 * 
	 * @see #HoldeeIteratorProxyImpl(HolderSide, HolderSide, boolean)
	 */
	@Override
	public Object interRemove( final Object proxy, final Method method) {
		getDelegateIterator().remove();
		
		final Logger logger = getLogger();
		
		final E holdee = getHoldeeEntity();
		
		if ( !isOrphanRemovalAttributeOn()) {
			final R defaultHolder = getDefaultHolderEntity();
			Collection<E> holdeesWithDefaultHolder = null;
				if ( defaultHolder != null) {
					holdeesWithDefaultHolder = defaultHolder.getHoldees();
				}
			if ( ( holdee != null) && holdee.equals( defaultHolder)) {
				if ( logger.isInfoEnabled()) {
					final String methodName = method.getName();
					logger.info(
							MessageFormat.format(
									"Prevented from setting {0} as holder of self in {1} " 
									+ "operation on {2} collection in {3}.", 
									holdee,
									methodName,
									holdee.getClass().getSimpleName(),
									getHolderEntity())
							);
				}
			}
			else {
				if ( holdeesWithDefaultHolder != null) {
					holdeesWithDefaultHolder.remove( holdee);	
						// to assure no duplicate by different instance 
					holdeesWithDefaultHolder.add( holdee);
					
						if ( logger.isTraceEnabled()) {
							logger.trace( 
									String.format(
											"Set downward link from default holder entity " 
											+ "to holdee entity after cutting downward link " 
											+ "from holder entity to holdee entity. " 
											+ "%nHolder entity: %1$s" 
											+ "%nHoldee eneity: %2$s" 
											+ "%nDefault holder eneity: %3$s",
											getHolderEntity(),
											holdee,
											defaultHolder)
									);
						}
				}
				if ( holdee != null) {
					holdee.setHolderEntity( defaultHolder);
						if ( logger.isTraceEnabled()) {
							logger.trace( 
									String.format(
											"Set upward link from holdee entity to default " 
											+ "holder entity after cutting relation between " 
											+ "holder entity and holdee entity. " 
											+ "%nHolder entity: %1$s" 
											+ "%nHoldee eneity: %2$s" 
											+ "%nDefault holder eneity: %3$s",
											getHolderEntity(),
											holdee,
											defaultHolder)
									);
						}
				}
			}
		}
		else {
			if ( isOkToNullifyHolderAtRemoval()) {
				if ( holdee != null) {
					holdee.setHolderEntity( null);
					
						if ( logger.isTraceEnabled()) {
							logger.trace( 
									String.format(
											"Nullified upward link to holder entity on holdee " 
											+ "entity after cutting relation between " 
											+ "holder entity and holdee entity. " 
											+ "%nHolder entity: %1$s" 
											+ "%nHoldee eneity: %2$s", 
											getHolderEntity(),
											holdee)
									);
						}
				}
			}
			else {
				if ( logger.isDebugEnabled()) {
					logger.debug(
							String.format(
									"Did not cut upward link from holdee entity to holder entity " 
									+ "by assigning null as holder on holdee entity because " 
									+ "orphanRemoval attribute has set to %1$b in %2$s holder " 
									+ "class and isOkToNullifyHolderAtRemoval switch value is %3$b."
									+ "%nHolder eneity: %4$s" 
									+ "%nHoldee entity: %5$s", 
									isOrphanRemovalAttributeOn,
									getHolderEntity().getClass().getSimpleName(),
									isOkToNullifyHolderAtRemoval,
									getHolderEntity(),
									holdee)
							);
				}
			}
		}
		
		return null;
	}
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object invoke( final Object proxy, final Method method, final Object[] args)
	throws Throwable {
		final Class<?>[] parameterTypes = method.getParameterTypes();
		if ( parameterTypes.length < 1) {
			final String methodName = method.getName();
			switch( methodName) {
			case "next":
				return interNext( proxy, method);
			case "remove":
				return interRemove( proxy, method);
			}
		}
		
		return method.invoke( getDelegateIterator(), args);
	}
}